﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Data.SqlClient;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Drawing;
using System.Drawing.Imaging;
using Panaglobe.Silverlight.PhotoBrowser.Server;


/// <summary>
/// This is a sample implementation of webservice for PhotoBrowser application.
/// You can create own service, but it have implement <see cref="IPhotoBrowserService"/> interface.
/// Or you can change this, but ensure the compatibility.
/// This service provides entire functionality to client application.
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class PhotoBrowserService : WebService, IPhotoBrowserService
{

    /// <summary>
    /// Initializes a new instance of the <see cref="PhotoBrowserService"/> class.
    /// </summary>
    public PhotoBrowserService() { }

    /// <summary>
    /// Gets the array of <see cref="ThumbnailInfo"/> instances
    /// those contain information about
    /// images in specified album.
    /// This method will be called from client side until the result
    /// will an empty array.
    /// </summary>
    /// <param name="albumID">The identifier of parent album.</param>
    /// <param name="startIndex">The zero-based index of first requested element in the album.</param>
    /// <param name="count">The count of requested elements. The result can be less, but not more.</param>
    /// <returns>
    /// An array of <see cref="ThumbnailInfo"/> instances those resulted by a server query from startindex.
    /// </returns>
    /// <remarks>
    /// The operation flow: The client requests the first ten elements,
    /// then second ten elements and so on, until the result is an empty array, or <c>null</c>.
    /// </remarks>
    [WebMethod]
    public ThumbnailInfo[] GetThumbnailsArray(string albumID, int startIndex, int count)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandText = "SELECT TOP " + startIndex + count + " * FROM Photos WHERE PhotoAlbumID = @PhotoAlbumID ORDER BY DateCreated";
        command.Parameters.Add("@PhotoAlbumID", SqlDbType.UniqueIdentifier).Value = new Guid(albumID);

        List<ThumbnailInfo> thumbnailsList = new List<ThumbnailInfo>();

        int index = 0;
        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            while (reader.Read())
            {
                if (index >= startIndex)
                    thumbnailsList.Add(this.CreateThumbnailInfoFromReader(reader));
                index++;
            }
        }

        return thumbnailsList.ToArray();
    }

    /// <summary>
    /// Creates a new instance of <see cref="ThumbnailInfo"/> class from passed instance of <see cref="DbDataReader"/>.
    /// </summary>
    /// <param name="reader">The reader that contains data about image in album. It must stay on a valid row.</param>
    /// <returns>A new instance of <see cref="ThumbnailInfo"/> class.</returns>
    private ThumbnailInfo CreateThumbnailInfoFromReader(DbDataReader reader)
    {
        return new ThumbnailInfo()
        {
            ID = ((Guid)reader["ID"]).ToString(),
            ContentType = (string)reader["ContentType"],
            CanEdit = this.GetPhotoCanEdit(((Guid)reader["ID"]).ToString()),
            Description = (string)reader["Description"],
            Title = (string)reader["Title"],
            BaseColor = this.GetBaseColor((byte[])reader["Thumbnail"])
        };
    }

    /// <summary>
    /// Gets an instance of <see cref="ThumbnailInfo"/> class that contains information about specified image.
    /// </summary>
    /// <param name="id">The identifier of image.</param>
    /// <returns>
    /// An instance of <see cref="ThumbnailInfo"/> class that contains information. The result can be <c>null</c> if the image not found.
    /// </returns>
    [WebMethod]
    public ThumbnailInfo GetThumbnailForImage(string id)
    {

        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandText = "SELECT * FROM Photos WHERE ID = @ID";
        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);

        ThumbnailInfo result = null;

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                result = this.CreateThumbnailInfoFromReader(reader);
        }

        return result;

    }

    /// <summary>
    /// Deletes the specified image.
    /// </summary>
    /// <param name="id">The identifier of image to delete.</param>
    [WebMethod]
    public void DeleteImage(string id)
    {
        if (!GetPhotoCanEdit(id))
            throw new AccessDeniedException();

        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandText = "DELETE FROM Photos WHERE ID = @ID";
        command.CommandType = CommandType.Text;
        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        command.ExecuteNonQuery();
    }

    /// <summary>
    /// Gets the thumbnail image byte array of the specified image.
    /// This is the image in raw format (as is).
    /// </summary>
    /// <param name="id">The identifier of image.</param>
    /// <returns>
    /// An array of <see cref="byte"/> elements that contains the thumbnail image in raw format.
    /// Can be <c>null</c> when the specified image not found.
    /// </returns>
    [WebMethod]
    public byte[] GetThumbnailImage(string id)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "SELECT Thumbnail FROM Photos WHERE ID = @ID";

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);

        byte[] result = null;
        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                result = (byte[])reader["Thumbnail"];
        }

        return result;
    }

    /// <summary>
    /// Gets the large image byte arra of specified image in raw format.
    /// </summary>
    /// <param name="id">The identifier of image.</param>
    /// <returns>
    /// An array of <see cref="byte"/> elements that contains the entire image in raw format.
    /// This is a file stream in byte array.
    /// Can be <c>null</c> when the specified image not found.
    /// </returns>
    [WebMethod]
    public byte[] GetImage(string id)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandText = "SELECT LargeImage FROM Photos WHERE ID = @ID";
        command.CommandType = CommandType.Text;

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        byte[] result = null;

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                result = (byte[])reader["LargeImage"];
        }

        return result;

    }

    /// <summary>
    /// Gets the base color by an image. This color will be background color of PhotoBrowser application.
    /// </summary>
    /// <param name="id">The identifier of image.</param>
    /// <returns>
    /// A <see cref="ColorInfo"/> instance that contains the base color with ARGB data.
    /// Can be <c>null</c> when the specified image not found.
    /// </returns>
    [WebMethod]
    public ColorInfo GetBaseColorForImage(string id)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandText = "SELECT Thumbnail FROM Photos WHERE ID = @ID";
        command.CommandType = CommandType.Text;

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        ColorInfo result = null;

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                result = this.GetBaseColor((byte[])reader["Thumbnail"]);
        }

        return result;
    }

    /// <summary>
    /// Calculates a base color from a passed thumbnail stream by averaging.
    /// </summary>
    /// <param name="thumbnailStream">The thumbnail stream that represented by an byte array.</param>
    /// <returns>A new instance of <see cref="ColorInfo"/> class that contains the base color of passed thumbnail.</returns>
    private ColorInfo GetBaseColor(byte[] thumbnailStream)
    {
        int redTotal = 0;
        int greenTotal = 0;
        int blueTotal = 0;

        ImageConverter converter = new ImageConverter();
        Bitmap bitmap = converter.ConvertFrom(thumbnailStream) as Bitmap;

        for (int x = 0; x < bitmap.Width; x++)
        {
            for (int y = 0; y < bitmap.Height; y++)
            {
                Color pixelColor = bitmap.GetPixel(x, y);
                redTotal += pixelColor.R;
                greenTotal += pixelColor.G;
                blueTotal += pixelColor.B;
            }
        }

        int pixelCount = bitmap.Width * bitmap.Height;
        byte red = (byte)Math.Round((double)redTotal / (double)pixelCount);
        byte green = (byte)Math.Round((double)greenTotal / (double)pixelCount);
        byte blue = (byte)Math.Round((double)blueTotal / (double)pixelCount);

        return new ColorInfo() { Alpha = 0xFF, Red = red, Green = green, Blue = blue };
    }

    /// <summary>
    /// Updates the title and description of specified image.
    /// </summary>
    /// <param name="id">The identifier of image to update.</param>
    /// <param name="title">The new title.</param>
    /// <param name="description">The new description.</param>
    [WebMethod]
    public void UpdateImageData(string id, string title, string description)
    {
        if (!GetPhotoCanEdit(id))
            throw new AccessDeniedException();

        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandText = "UPDATE Photos SET Title = @Title, Description = @Description, DateModified = GetDate() WHERE ID = @ID";
        command.CommandType = CommandType.Text;

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        command.Parameters.Add("@Title", SqlDbType.NVarChar, 400).Value = title;
        command.Parameters.Add("@Description", SqlDbType.NVarChar, description.Length).Value = description;

        command.ExecuteNonQuery();
    }

    /// <summary>
    /// Creates a new <see cref="ImageSlot"/> and returns with a <see cref="ImageSlotInfo"/>
    /// class to receive uploaded chunk.
    /// This class will provide functionality to handle image uploading.
    /// </summary>
    /// <param name="albumID">The album identifier that will contain the uploaded image.</param>
    /// <param name="fileName">The original filename to download operations. For example: "imgfile.jpg".</param>
    /// <returns>
    /// An instance of <see cref="ImageSlotInfo"/> class that contains information about image slot to be created.
    /// </returns>
    [WebMethod(EnableSession = true)]
    public ImageSlotInfo GetImageSlot(string albumID, string fileName)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandText = "SELECT COUNT(ID) as PhotoCount FROM Photos WHERE PhotoAlbumID = @PhotoAlbumID";
        command.CommandType = CommandType.Text;
        command.Parameters.Add("@PhotoAlbumID", SqlDbType.UniqueIdentifier).Value = new Guid(albumID);

        int photoCount = 0;
        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                photoCount = (int)reader["PhotoCount"];
        }

        ImageSlot slot = ImageSlot.CreateImageSlot(albumID, fileName, new StoreUploadedImageCallback(ImageStoringCallback), photoCount == 0);
        return new ImageSlotInfo() { ImageSlotID = slot.ID, ImageID = slot.ID.ToString() };
    }



    /// <summary>
    /// Called by <see cref="ImageSlot"/> instance to store uploaded image.
    /// </summary>
    /// <param name="imageSlotToStore">The <see cref="ImageSlot"/> instance that has called this function.</param>
    /// <param name="contentType">The content type of uploaded image to be stored.</param>
    /// <param name="largeImage">The byte array of large image.</param>
    /// <param name="thumbnail">The byte array of thumbnail image.</param>
    private void ImageStoringCallback(ImageSlot imageSlotToStore, string contentType, byte[] largeImage, byte[] thumbnail)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "INSERT Photos VALUES (@ID, @PhotoAlbumID, @Title, @Description, @ContentType, @FileName, @LargeImage, @Thumbnail, GetDate(), GetDate())";
        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = imageSlotToStore.ID;
        command.Parameters.Add("@PhotoAlbumID", SqlDbType.UniqueIdentifier).Value = new Guid(imageSlotToStore.AlbumID);
        command.Parameters.Add("@Title", SqlDbType.NVarChar, 400).Value = imageSlotToStore.Title;
        command.Parameters.Add("@Description", SqlDbType.NVarChar, imageSlotToStore.Description.Length).Value = imageSlotToStore.Description;
        command.Parameters.Add("@ContentType", SqlDbType.VarChar, 255).Value = contentType;
        command.Parameters.Add("@FileName", SqlDbType.NVarChar, 255).Value = imageSlotToStore.FileName;
        command.Parameters.Add("@LargeImage", SqlDbType.Image, largeImage.Length).Value = largeImage;
        command.Parameters.Add("@Thumbnail", SqlDbType.Image, thumbnail.Length).Value = thumbnail;

        command.ExecuteNonQuery();

    }

    /// <summary>
    /// Appends the next image chunk of image. Call <see cref="GetImageSlot"/> method before this.
    /// </summary>
    /// <param name="imageSlotID">The identifier of image slot.</param>
    /// <param name="chunk">The chunk that is an array of bytes that contains next package.</param>
    /// <param name="lastChunk">if set to <c>true</c> this is the last chunk, so the server can close imageslot and resize, save the uploaded image. Otherwise it will be next chunk.</param>
    /// <param name="title">The title of image. This can be <c>null</c>. In this case it will not be handled.</param>
    /// <param name="description">The title of image. This can be <c>null</c>. In this case it will not be handled.</param>
    /// <remarks>
    /// The title and description arguments get value in last chunk only.
    /// </remarks>
    [WebMethod(EnableSession = true)]
    public void AppendImageChunk(Guid imageSlotID, byte[] chunk, bool lastChunk,
        string title, string description)
    {
        ImageSlot slot = ImageSlot.GetImageSlot(imageSlotID);

        if (!string.IsNullOrEmpty(title) || lastChunk)
            slot.Title = title;

        if (!string.IsNullOrEmpty(description) || lastChunk)
            slot.Description = description;

        if (lastChunk && !GetAlbumCanEdit(slot.AlbumID))
            throw new AccessDeniedException();

        slot.AppendChunk(chunk, lastChunk);

        if (lastChunk && slot.CreateThumbnail)
            this.SetImageToAlbumThumbnail(slot.ID.ToString());

    }

    /// <summary>
    /// Aborts the image slot (and upload operation). The slot will be cleaned up.
    /// </summary>
    /// <param name="id">The identifier of image slot.</param>
    [WebMethod(EnableSession = true)]
    public void AbortImageSlot(Guid id)
    {
        ImageSlot slot = ImageSlot.GetImageSlot(id);
        slot.Abort();
    }


    /// <summary>
    /// Gets the information about specified album and returns with a new instance of <see cref="AlbumInfo"/> class.
    /// </summary>
    /// <param name="albumID">The album identifier.</param>
    /// <returns>
    /// An instance of <see cref="AlbumInfo"/> class that contains information about specified album.
    /// This can be <c>null</c> when the album not found.
    /// </returns>
    [WebMethod]
    public AlbumInfo GetAlbumInfo(string albumID)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "SELECT * FROM PhotoAlbums WHERE ID = @ID";

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(albumID);


        AlbumInfo info = null;

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
            {
                info = new AlbumInfo()
                {
                    ID = ((Guid)reader["ID"]).ToString(),
                    Title = (string)reader["Title"],
                    Description = (string)reader["Description"],
                    CanEdit = this.GetAlbumCanEdit(((Guid)reader["ID"]).ToString()),
                    HasThumbnail = (!string.IsNullOrEmpty(reader["ThumbnailContentType"] as string)
                        && reader["Thumbnail"] is byte[] && ((byte[])reader["Thumbnail"]).Length > 0)
                };
            }
        }

        return info;
    }

    /// <summary>
    /// Sets the title and description of specified album.
    /// </summary>
    /// <param name="id">The identifier of album.</param>
    /// <param name="title">The new title of album.</param>
    /// <param name="description">The new description of album.</param>
    [WebMethod]
    public void SetAlbumProperties(string id, string title, string description)
    {
        if (!GetAlbumCanEdit(id))
            throw new AccessDeniedException();

        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "UPDATE PhotoAlbums SET Title = @Title, Description = @Description, DateModified = GetDate() WHERE ID = @ID";
        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        command.Parameters.Add("@Title", SqlDbType.NVarChar, 400).Value = title;
        command.Parameters.Add("@Description", SqlDbType.NVarChar, description.Length).Value = description;
        command.ExecuteNonQuery();
    }

    /// <summary>
    /// Creates an icon for album from specified image.
    /// </summary>
    /// <param name="id">The identifier of image.</param>
    [WebMethod]
    public void SetImageToAlbumThumbnail(string id)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandText = "SELECT * FROM Photos WHERE ID = @ID";
        command.CommandType = CommandType.Text;

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        byte[] arrayOfImageBytes = null;
        Guid albumID = Guid.Empty;

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
            {
                arrayOfImageBytes = (byte[])reader["LargeImage"];
                albumID = (Guid)reader["PhotoAlbumID"];
            }
        }

        if (!GetAlbumCanEdit(albumID.ToString()))
            throw new AccessDeniedException();

        ImageConverter converter = new ImageConverter();
        ImageResizer resizer = new ImageResizer(converter.ConvertFrom(arrayOfImageBytes) as Bitmap);

        resizer.ResizeMode = ResizeMode.UniformToFill;
        resizer.CroppedCanvas = false;
        Bitmap resizedBitmap = resizer.ResizeTo(new Size(64, 64));

        string contentType = string.Empty;
        if (resizedBitmap.RawFormat.Guid == ImageFormat.Jpeg.Guid)
            contentType = "image/jpeg";
        else if (resizedBitmap.RawFormat.Guid == ImageFormat.Gif.Guid)
            contentType = "image/gif";
        else if (resizedBitmap.RawFormat.Guid == ImageFormat.Png.Guid)
            contentType = "image/png";

        if (!string.IsNullOrEmpty(contentType))
        {
            arrayOfImageBytes = converter.ConvertTo(resizedBitmap, typeof(byte[])) as byte[];

            connection = WebCommon.CreateConnection();
            command = new SqlCommand();
            command.Connection = connection;

            command.CommandType = CommandType.Text;
            command.CommandText = "UPDATE PhotoAlbums SET Thumbnail = @Thumbnail, ThumbnailContentType = @ThumbnailContentType, DateModified = GetDate() WHERE ID = @ID";
            command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = albumID;
            command.Parameters.Add("@Thumbnail", SqlDbType.Image, arrayOfImageBytes.Length).Value = arrayOfImageBytes;
            command.Parameters.Add("@ThumbnailContentType", SqlDbType.VarChar, 255).Value = contentType;

            command.ExecuteNonQuery();
        }

    }

    /// <summary>
    /// Gets byte array of icon from album. This is an image in raw format.
    /// </summary>
    /// <param name="id">The identifier of album.</param>
    /// <returns>
    /// A byte array that contains entire icon file of album. Can be <c>null</c> when the album not found or no icon specified.
    /// </returns>
    [WebMethod]
    public byte[] GetAlbumThumbnail(string id)
    {
        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "SELECT Thumbnail FROM PhotoAlbums WHERE ID = @ID";

        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);

        byte[] result = null;
        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            if (reader.Read())
                if (reader["Thumbnail"] is byte[])
                    result = (byte[])reader["Thumbnail"];
        }

        return result;
    }

    /// <summary>
    /// Deletes the specified album and its entire content.
    /// </summary>
    /// <param name="id">The identifier of album to delete.</param>
    [WebMethod]
    public void DeleteAlbum(string id)
    {
        if (!GetAlbumCanEdit(id))
            throw new AccessDeniedException();

        SqlConnection connection = WebCommon.CreateConnection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;

        command.CommandType = CommandType.Text;
        command.CommandText = "DELETE FROM PhotoAlbums WHERE ID = @ID";
        command.Parameters.Add("@ID", SqlDbType.UniqueIdentifier).Value = new Guid(id);
        command.ExecuteNonQuery();
    }

    /// <summary>
    /// Retrieves that the passed album can be edited on client side.
    /// </summary>
    /// <param name="albumID">The album identifier.</param>
    /// <returns>If <c>true</c> the album can be edited on client side and the browser will allow edit operations on album; otherwise <c>false</c>.</returns>
    private bool GetAlbumCanEdit(string albumID)
    {
        return HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated;
    }

    /// <summary>
    /// Retrieves that the passed image can be edited on client side.
    /// </summary>
    /// <param name="albumID">The image identifier.</param>
    /// <returns>If <c>true</c> the photo can be edited on client side and the browser will allow edit operations on specified photo; otherwise <c>false</c>.</returns>
    private bool GetPhotoCanEdit(string id)
    {
        return HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated;
    }
}

